<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>流式数据分段读取与断点续读</title>
    <style>
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      }

      body {
        background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
        color: #fff;
        min-height: 100vh;
        padding: 20px;
        display: flex;
        flex-direction: column;
        align-items: center;
      }

      .container {
        width: 100%;
        max-width: 800px;
        background: rgba(0, 0, 0, 0.7);
        border-radius: 15px;
        padding: 25px;
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
        margin: 20px 0;
      }

      h1 {
        text-align: center;
        margin-bottom: 20px;
        color: #fdbb2d;
        font-size: 2.5rem;
        text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
      }

      h2 {
        margin: 25px 0 15px;
        color: #4dabf7;
        border-bottom: 2px solid #4dabf7;
        padding-bottom: 8px;
      }

      .section {
        margin-bottom: 25px;
        background: rgba(30, 30, 46, 0.6);
        padding: 20px;
        border-radius: 12px;
      }

      .controls {
        display: flex;
        flex-wrap: wrap;
        gap: 15px;
        margin: 20px 0;
      }

      button {
        background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
        color: white;
        border: none;
        padding: 12px 25px;
        border-radius: 50px;
        cursor: pointer;
        font-size: 1rem;
        font-weight: 600;
        transition: all 0.3s ease;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
        flex: 1;
        min-width: 150px;
      }

      button:hover {
        transform: translateY(-3px);
        box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
      }

      button:active {
        transform: translateY(1px);
      }

      button:disabled {
        background: linear-gradient(to right, #5c677d 0%, #7b879d 100%);
        cursor: not-allowed;
        transform: none;
        box-shadow: none;
      }

      .btn-stop {
        background: linear-gradient(to right, #ff416c, #ff4b2b);
      }

      .btn-resume {
        background: linear-gradient(to right, #00b09b, #96c93d);
      }

      .progress-container {
        width: 100%;
        background: #333;
        border-radius: 10px;
        margin: 20px 0;
        overflow: hidden;
      }

      .progress-bar {
        height: 30px;
        background: linear-gradient(90deg, #00c6ff, #0072ff);
        border-radius: 10px;
        width: 0%;
        transition: width 0.3s ease;
        display: flex;
        align-items: center;
        justify-content: center;
        color: white;
        font-weight: bold;
        text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
      }

      textarea {
        width: 100%;
        height: 150px;
        background: #222;
        color: #00ff9d;
        border: 2px solid #4dabf7;
        border-radius: 10px;
        padding: 15px;
        font-family: monospace;
        font-size: 14px;
        resize: vertical;
        margin: 15px 0;
      }

      .log {
        background: #111;
        border-radius: 10px;
        padding: 15px;
        height: 200px;
        overflow-y: auto;
        margin-top: 20px;
        font-family: monospace;
        font-size: 14px;
      }

      .log-entry {
        margin: 8px 0;
        padding: 8px;
        border-radius: 5px;
        background: rgba(255, 255, 255, 0.05);
      }

      .log-success {
        color: #00ff9d;
        border-left: 4px solid #00ff9d;
      }

      .log-warning {
        color: #ffcc00;
        border-left: 4px solid #ffcc00;
      }

      .log-error {
        color: #ff6b6b;
        border-left: 4px solid #ff6b6b;
      }

      .status {
        display: flex;
        justify-content: space-between;
        margin: 15px 0;
        font-size: 1.1rem;
      }

      .status-item {
        background: rgba(77, 171, 247, 0.2);
        padding: 12px;
        border-radius: 8px;
        flex: 1;
        margin: 0 5px;
        text-align: center;
      }

      .chunk-size-control {
        display: flex;
        align-items: center;
        gap: 15px;
        margin: 15px 0;
      }

      input[type='range'] {
        flex: 1;
        height: 10px;
        -webkit-appearance: none;
        background: #333;
        border-radius: 5px;
        outline: none;
      }

      input[type='range']::-webkit-slider-thumb {
        -webkit-appearance: none;
        width: 25px;
        height: 25px;
        border-radius: 50%;
        background: #4dabf7;
        cursor: pointer;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
      }

      .chunk-size-value {
        background: rgba(77, 171, 247, 0.3);
        padding: 8px 15px;
        border-radius: 20px;
        min-width: 100px;
        text-align: center;
      }

      .explanation {
        background: rgba(30, 30, 46, 0.6);
        padding: 20px;
        border-radius: 12px;
        margin-top: 25px;
      }

      .explanation h3 {
        color: #fdbb2d;
        margin-bottom: 15px;
      }

      .explanation ul {
        padding-left: 25px;
        margin: 15px 0;
      }

      .explanation li {
        margin: 10px 0;
        line-height: 1.6;
      }

      .code {
        background: #222;
        padding: 15px;
        border-radius: 8px;
        font-family: monospace;
        margin: 15px 0;
        color: #4dabf7;
        overflow-x: auto;
      }

      @media (max-width: 600px) {
        .controls {
          flex-direction: column;
        }

        .status {
          flex-direction: column;
          gap: 10px;
        }
      }
    </style>
  </head>
  <body>
    <h1>流式数据分段读取与断点续读</h1>

    <div class="container">
      <div class="section">
        <h2>数据源设置</h2>
        <textarea id="sourceData">
这是一个演示流式数据分段读取的示例文本。流式处理允许我们处理大型数据集而无需一次性加载所有内容到内存中。通过分段读取，我们可以处理任意大小的数据，同时保持良好的性能表现。JavaScript中的ReadableStream API为这种处理模式提供了强大的支持。</textarea
        >
        <button id="createStream">创建可分段读取流</button>
      </div>

      <div class="section">
        <h2>读取控制</h2>
        <div class="chunk-size-control">
          <label for="chunkSize">读取块大小:</label>
          <input type="range" id="chunkSize" min="1" max="20" value="5" />
          <div class="chunk-size-value"><span id="chunkSizeValue">5</span> 个字符</div>
        </div>

        <div class="controls">
          <button id="startReading" disabled>开始读取</button>
          <button id="pauseReading" class="btn-stop" disabled>暂停读取</button>
          <button id="resumeReading" class="btn-resume" disabled>继续读取</button>
          <button id="resetReader" disabled>重置读取器</button>
        </div>

        <div class="progress-container">
          <div class="progress-bar" id="progressBar">0%</div>
        </div>

        <div class="status">
          <div class="status-item">已读取: <span id="bytesRead">0</span> 字节</div>
          <div class="status-item">剩余: <span id="bytesRemaining">0</span> 字节</div>
          <div class="status-item">进度: <span id="progressPercent">0</span>%</div>
        </div>
      </div>

      <div class="section">
        <h2>读取结果</h2>
        <textarea id="result" placeholder="读取的数据将显示在这里..." readonly></textarea>
      </div>

      <div class="section">
        <h2>操作日志</h2>
        <div class="log" id="log"></div>
      </div>
    </div>

    <div class="container explanation">
      <h3>实现原理说明</h3>
      <p>本演示展示了如何实现流的分段读取和断点续读功能：</p>

      <ul>
        <li>
          <strong>封装可恢复读取器</strong>：创建
          <code>ResumableStreamReader</code> 类，内部管理读取状态
        </li>
        <li>
          <strong>暂停机制</strong>：使用 <code>pauseController</code> 控制读取流程的暂停和恢复
        </li>
        <li><strong>状态保存</strong>：保存读取位置和缓冲区，确保恢复时从正确位置继续</li>
        <li><strong>数据拼接</strong>：将每次读取的数据块拼接到结果中</li>
        <li><strong>进度跟踪</strong>：实时计算和显示读取进度</li>
      </ul>

      <div class="code">
        // 核心代码片段 class ResumableStreamReader { constructor(stream) { this.stream = stream;
        this.reader = null; this.position = 0; this.buffer = ""; this.isReading = false;
        this.pauseController = null; } async readNextChunk(maxChars) { if (!this.reader) this.reader
        = this.stream.getReader(); this.isReading = true; let chunk = ''; while (chunk.length <
        maxChars && this.isReading) { const { value, done } = await this.reader.read(); if (done)
        break; chunk += value; this.position += value.length; } this.buffer += chunk; return chunk;
        } pause() { this.isReading = false; if (this.pauseController) {
        this.pauseController.resolve(); this.pauseController = null; } } async resume() {
        this.pauseController = null; this.isReading = true; } reset() { this.pause(); if
        (this.reader) { this.reader.releaseLock(); this.reader = null; } this.position = 0;
        this.buffer = ""; } }
      </div>
    </div>

    <script>
      class ResumableStreamReader {
        constructor(stream) {
          this.stream = stream;
          this.reader = null;
          this.position = 0; // 当前读取位置
          this.buffer = ''; // 已读取但尚未处理的缓冲区
          this.isReading = false;
          this.pauseController = null;
          this.totalBytes = null;
        }

        async readNextChunk(maxChars) {
          if (!this.reader) {
            this.reader = this.stream.getReader();
          }

          this.isReading = true;
          let chunk = '';

          // 读取直到达到指定字符数或流结束
          while (chunk.length < maxChars && this.isReading) {
            const { value, done } = await this.reader.read();
            if (done) break;

            chunk += value;
            this.position += value.length;
          }

          // 添加到缓冲区
          this.buffer += chunk;
          return chunk;
        }

        // 暂停读取
        pause() {
          this.isReading = false;
          if (this.pauseController) {
            this.pauseController.resolve();
            this.pauseController = null;
          }
        }

        // 继续读取
        async resume() {
          this.pauseController = null;
          this.isReading = true;
        }

        // 重置读取器
        reset() {
          this.pause();
          if (this.reader) {
            this.reader.releaseLock();
            this.reader = null;
          }
          this.position = 0;
          this.buffer = '';
        }

        // 获取当前读取位置
        getPosition() {
          return this.position;
        }

        // 获取缓冲区内容
        getBuffer() {
          return this.buffer;
        }

        // 清空缓冲区
        clearBuffer() {
          const content = this.buffer;
          this.buffer = '';
          return content;
        }
      }

      // DOM元素
      const sourceDataEl = document.getElementById('sourceData');
      const resultEl = document.getElementById('result');
      const createStreamBtn = document.getElementById('createStream');
      const startReadingBtn = document.getElementById('startReading');
      const pauseReadingBtn = document.getElementById('pauseReading');
      const resumeReadingBtn = document.getElementById('resumeReading');
      const resetReaderBtn = document.getElementById('resetReader');
      const progressBarEl = document.getElementById('progressBar');
      const bytesReadEl = document.getElementById('bytesRead');
      const bytesRemainingEl = document.getElementById('bytesRemaining');
      const progressPercentEl = document.getElementById('progressPercent');
      const chunkSizeSlider = document.getElementById('chunkSize');
      const chunkSizeValue = document.getElementById('chunkSizeValue');
      const logEl = document.getElementById('log');

      // 全局变量
      let resumableReader = null;
      let stream = null;
      let totalBytes = 0;
      let isReadingActive = false;

      // 添加日志
      function addLog(message, type = 'success') {
        console.log(message);

        const logEntry = document.createElement('div');
        logEntry.className = `log-entry log-${type}`;
        logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
        logEl.appendChild(logEntry);
        logEl.scrollTop = logEl.scrollHeight;
      }

      // 更新进度
      function updateProgress() {
        if (!resumableReader || totalBytes === 0) return;

        const position = resumableReader.getPosition();
        const percent = Math.min(100, Math.round((position / totalBytes) * 100));

        progressBarEl.style.width = `${percent}%`;
        progressBarEl.textContent = `${percent}%`;
        bytesReadEl.textContent = position;
        bytesRemainingEl.textContent = totalBytes - position;
        progressPercentEl.textContent = percent;
      }

      // 创建可分段读取的流
      function createResumableStream(text) {
        return new ReadableStream({
          start(controller) {
            const length = text.length;
            let index = 0;
            const tiemr = setInterval(() => {
              controller.enqueue(text[index]);
              index++;
              if (index === length) {
                controller.close();
                clearInterval(tiemr);
              }
            }, 1); // 
          },
        });
      }

      // 开始读取流
      async function startReading() {
        if (!resumableReader) {
          addLog('错误: 请先创建流', 'error');
          return;
        }

        isReadingActive = true;
        startReadingBtn.disabled = true;
        pauseReadingBtn.disabled = false;
        resumeReadingBtn.disabled = true;

        addLog('开始读取流数据...');

        while (isReadingActive && resumableReader.getPosition() < totalBytes) {
          const chunkSize = parseInt(chunkSizeSlider.value);

          try {
            const chunk = await resumableReader.readNextChunk(chunkSize);
            if (chunk) {
              resultEl.value += chunk;
              addLog(`成功读取 ${chunk.length} 个字符`);
              updateProgress();

              // 添加一点延迟，使读取过程可见
              await new Promise(resolve => setTimeout(resolve, 500));
            }
          } catch (error) {
            addLog(`读取错误: ${error.message}`, 'error');
            break;
          }

          // 如果到达末尾
          if (resumableReader.getPosition() >= totalBytes) {
            addLog('流读取完成!', 'success');
            break;
          }
        }

        isReadingActive = false;
        startReadingBtn.disabled = false;
        pauseReadingBtn.disabled = true;
        resumeReadingBtn.disabled = true;
      }

      // 事件监听器
      createStreamBtn.addEventListener('click', () => {
        const text = sourceDataEl.value;
        if (!text.trim()) {
          addLog('错误: 请输入源数据', 'error');
          return;
        }

        stream = createResumableStream(text);
        resumableReader = new ResumableStreamReader(stream);
        totalBytes = text.length;

        resultEl.value = '';
        progressBarEl.style.width = '0%';
        progressBarEl.textContent = '0%';
        bytesReadEl.textContent = '0';
        bytesRemainingEl.textContent = totalBytes;
        progressPercentEl.textContent = '0';

        startReadingBtn.disabled = false;
        resetReaderBtn.disabled = false;

        addLog(`已创建流，总大小: ${totalBytes} 字节`);
        updateProgress();
      });

      startReadingBtn.addEventListener('click', startReading);

      pauseReadingBtn.addEventListener('click', () => {
        if (resumableReader && isReadingActive) {
          resumableReader.pause();
          isReadingActive = false;
          pauseReadingBtn.disabled = true;
          resumeReadingBtn.disabled = false;
          addLog('读取已暂停');
        }
      });

      resumeReadingBtn.addEventListener('click', () => {
        if (resumableReader) {
          resumableReader.resume();
          isReadingActive = true;
          pauseReadingBtn.disabled = false;
          resumeReadingBtn.disabled = true;
          addLog('继续读取...');
          startReading();
        }
      });

      resetReaderBtn.addEventListener('click', () => {
        if (resumableReader) {
          resumableReader.reset();
          resultEl.value = '';
          progressBarEl.style.width = '0%';
          progressBarEl.textContent = '0%';
          bytesReadEl.textContent = '0';
          bytesRemainingEl.textContent = totalBytes;
          progressPercentEl.textContent = '0';
          isReadingActive = false;

          startReadingBtn.disabled = false;
          pauseReadingBtn.disabled = true;
          resumeReadingBtn.disabled = true;

          addLog('读取器已重置');
        }
      });

      chunkSizeSlider.addEventListener('input', () => {
        const size = parseInt(chunkSizeSlider.value);
        chunkSizeValue.textContent = size;
      });

      // 初始化
      chunkSizeValue.textContent = chunkSizeSlider.value;
      addLog('准备好创建流并开始分段读取', 'success');
    </script>
  </body>
</html>
